{
 "cells": [
  {
   "cell_type": "code",
   "id": "initial_id",
   "metadata": {
    "collapsed": true,
    "ExecuteTime": {
     "end_time": "2025-02-07T01:34:24.673237Z",
     "start_time": "2025-02-07T01:34:21.242842Z"
    }
   },
   "source": [
    "# 导入库\n",
    "import matplotlib as mpl\n",
    "# Matplotlib 绘制的图形会直接嵌入到 Notebook 的输出单元格中，而不是弹出一个独立的窗口\n",
    "%matplotlib inline\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "\n",
    "# os库提供了一种使用操作系统相关功能的便捷方式，允许与操作系统进行交互\n",
    "import os\n",
    "\n",
    "# sys库主要用于处理Python运行时的环境相关信息以及与解释器的交互\n",
    "import sys\n",
    "import time\n",
    "\n",
    "# tqdm库是一个快速，可扩展的Python进度条，可以在 Python 长循环中添加一个进度提示信息，用户只需要封装任意的迭代器 tqdm(iterator)，即可获得一个进度条显示迭代进度。\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "\n",
    "# torch.nn是PyTorch中用于构建神经网络的模块\n",
    "import torch.nn as nn\n",
    "\n",
    "# torch.nn.functional是PyTorch中包含了神经网络的一些常用函数\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "\n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.6.0+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 数据准备",
   "id": "d5a84943fb6c9c69"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:34:37.675021Z",
     "start_time": "2025-02-07T01:34:37.586506Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from sklearn.datasets import fetch_california_housing\n",
    "\n",
    "housing = fetch_california_housing(data_home='data')\n",
    "print(housing.DESCR)\n",
    "print(housing.data.shape)\n",
    "print(housing.target.shape)"
   ],
   "id": "ec5437ae0f5c87e9",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".. _california_housing_dataset:\n",
      "\n",
      "California Housing dataset\n",
      "--------------------------\n",
      "\n",
      "**Data Set Characteristics:**\n",
      "\n",
      ":Number of Instances: 20640\n",
      "\n",
      ":Number of Attributes: 8 numeric, predictive attributes and the target\n",
      "\n",
      ":Attribute Information:\n",
      "    - MedInc        median income in block group\n",
      "    - HouseAge      median house age in block group\n",
      "    - AveRooms      average number of rooms per household\n",
      "    - AveBedrms     average number of bedrooms per household\n",
      "    - Population    block group population\n",
      "    - AveOccup      average number of household members\n",
      "    - Latitude      block group latitude\n",
      "    - Longitude     block group longitude\n",
      "\n",
      ":Missing Attribute Values: None\n",
      "\n",
      "This dataset was obtained from the StatLib repository.\n",
      "https://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html\n",
      "\n",
      "The target variable is the median house value for California districts,\n",
      "expressed in hundreds of thousands of dollars ($100,000).\n",
      "\n",
      "This dataset was derived from the 1990 U.S. census, using one row per census\n",
      "block group. A block group is the smallest geographical unit for which the U.S.\n",
      "Census Bureau publishes sample data (a block group typically has a population\n",
      "of 600 to 3,000 people).\n",
      "\n",
      "A household is a group of people residing within a home. Since the average\n",
      "number of rooms and bedrooms in this dataset are provided per household, these\n",
      "columns may take surprisingly large values for block groups with few households\n",
      "and many empty houses, such as vacation resorts.\n",
      "\n",
      "It can be downloaded/loaded using the\n",
      ":func:`sklearn.datasets.fetch_california_housing` function.\n",
      "\n",
      ".. rubric:: References\n",
      "\n",
      "- Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions,\n",
      "  Statistics and Probability Letters, 33 (1997) 291-297\n",
      "\n",
      "(20640, 8)\n",
      "(20640,)\n"
     ]
    }
   ],
   "execution_count": 2
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:34:49.174257Z",
     "start_time": "2025-02-07T01:34:49.169744Z"
    }
   },
   "cell_type": "code",
   "source": [
    "import pprint  #打印的格式比较 好看\n",
    "\n",
    "pprint.pprint(housing.data[0:2])\n",
    "print('-' * 50)\n",
    "pprint.pprint(housing.target[0:2])"
   ],
   "id": "d79c728a16ea6e30",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[ 8.32520000e+00,  4.10000000e+01,  6.98412698e+00,\n",
      "         1.02380952e+00,  3.22000000e+02,  2.55555556e+00,\n",
      "         3.78800000e+01, -1.22230000e+02],\n",
      "       [ 8.30140000e+00,  2.10000000e+01,  6.23813708e+00,\n",
      "         9.71880492e-01,  2.40100000e+03,  2.10984183e+00,\n",
      "         3.78600000e+01, -1.22220000e+02]])\n",
      "--------------------------------------------------\n",
      "array([4.526, 3.585])\n"
     ]
    }
   ],
   "execution_count": 3
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:34:55.076629Z",
     "start_time": "2025-02-07T01:34:55.036270Z"
    }
   },
   "cell_type": "code",
   "source": [
    "pprint.pprint(housing.target[0:2])\n",
    "# train_test_split 是用于将数据集随机拆分为训练集和测试集的工具\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "# 将完整数据集 housing.data 和对应的标签 housing.target 拆分为训练集（x_train_all 和 y_train_all）和测试集（x_test 和 y_test）\n",
    "#保留一部分数据作为测试集，用于最终评估模型的性能\n",
    "x_train_all, x_test, y_train_all, y_test = train_test_split(\n",
    "    housing.data, housing.target, random_state=7)\n",
    "\n",
    "# 将训练集 x_train_all 和 y_train_all 进一步拆分为训练集（x_train 和 y_train）和验证集（x_valid 和 y_valid）\n",
    "# 验证集用于在训练过程中评估模型性能，调整超参数，防止过拟合\n",
    "x_train, x_valid, y_train, y_valid = train_test_split(\n",
    "    x_train_all, y_train_all, random_state=11)\n",
    "\n",
    "dataset_maps = {\n",
    "    \"train\": [x_train, y_train],  #训练集\n",
    "    \"valid\": [x_valid, y_valid],  #验证集\n",
    "    \"test\": [x_test, y_test],  #测试集\n",
    "}  #把3个数据集都放到字典中"
   ],
   "id": "d3b90965819837ee",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([4.526, 3.585])\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:35:23.315285Z",
     "start_time": "2025-02-07T01:35:23.306900Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "scaler = StandardScaler()  #标准化\n",
    "\n",
    "# fit 方法：计算数据的统计量（如均值、标准差等）或学习数据的特征（如 PCA 的主成分），但不对数据进行转换。\n",
    "# transform 方法：使用 fit 方法学习到的统计量或特征对数据进行转换。\n",
    "# fit_transform 方法：先调用 fit 方法学习统计量或特征，然后调用 transform 方法对数据进行转换。\n",
    "scaler.fit(x_train)"
   ],
   "id": "d79486ae35c93f09",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "StandardScaler()"
      ],
      "text/html": [
       "<style>#sk-container-id-1 {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: #000;\n",
       "  --sklearn-color-text-muted: #666;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-icon: #696969;\n",
       "\n",
       "  @media (prefers-color-scheme: dark) {\n",
       "    /* Redefinition of color scheme for dark theme */\n",
       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-icon: #878787;\n",
       "  }\n",
       "}\n",
       "\n",
       "#sk-container-id-1 {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip: rect(1px 1px 1px 1px);\n",
       "  clip: rect(1px, 1px, 1px, 1px);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       "#sk-container-id-1 label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "  align-items: start;\n",
       "  justify-content: space-between;\n",
       "  gap: 0.5em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label .caption {\n",
       "  font-size: 0.6rem;\n",
       "  font-weight: lighter;\n",
       "  color: var(--sklearn-color-text-muted);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content {\n",
       "  max-height: 0;\n",
       "  max-width: 0;\n",
       "  overflow: hidden;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  max-height: 200px;\n",
       "  max-width: 100%;\n",
       "  overflow: auto;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       "#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       "#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       "#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       "#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  display: inline-block;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       "#sk-container-id-1 div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       "#sk-container-id-1 div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 0.5em;\n",
       "  text-align: center;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "#sk-container-id-1 a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>StandardScaler()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow\"><div><div>StandardScaler</div></div><div><a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.6/modules/generated/sklearn.preprocessing.StandardScaler.html\">?<span>Documentation for StandardScaler</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></div></label><div class=\"sk-toggleable__content fitted\"><pre>StandardScaler()</pre></div> </div></div></div></div>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 5
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 构建数据集",
   "id": "c9a7e7cf2509edbe"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:35:46.168340Z",
     "start_time": "2025-02-07T01:35:46.157339Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 从PyTorch的utils.data模块导入Dataset类，并定义一个名为HousingDataset的新类，继承自Dataset。\n",
    "# Dataset是PyTorch中用于表示数据集的抽象基类\n",
    "# 通过继承Dataset类，我们可以自定义数据集的行为，使其与PyTorch的数据加载器兼容。\n",
    "from torch.utils.data import Dataset\n",
    "\n",
    "\n",
    "class HousingDataset(Dataset):\n",
    "\n",
    "    # 初始化方法，根据指定的模式加载数据，并对数据进行预处理\n",
    "    def __init__(self, mode='train'):\n",
    "        self.x, self.y = dataset_maps[mode]\n",
    "        self.x = torch.from_numpy(scaler.transform(self.x)).float()\n",
    "        self.y = torch.from_numpy(self.y).float().reshape(-1, 1)\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        return self.x[idx], self.y[idx]\n",
    "\n",
    "\n",
    "train_ds = HousingDataset(\"train\")\n",
    "valid_ds = HousingDataset(\"valid\")\n",
    "test_ds = HousingDataset(\"test\")"
   ],
   "id": "b00e4e0654c08ed2",
   "outputs": [],
   "execution_count": 6
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:35:52.028189Z",
     "start_time": "2025-02-07T01:35:52.022189Z"
    }
   },
   "cell_type": "code",
   "source": "type(train_ds)",
   "id": "9a0ba08a8f9ba26b",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "__main__.HousingDataset"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 7
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# DataLoader",
   "id": "c8dd0c4a47bc4c40"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:36:13.630597Z",
     "start_time": "2025-02-07T01:36:13.626598Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from torch.utils.data import DataLoader\n",
    "\n",
    "#放到DataLoader中的train_ds, valid_ds, test_ds都是dataset类型的数据\n",
    "batch_size = 16  #batch_size是可以调的超参数\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)"
   ],
   "id": "b1931744cdb82b10",
   "outputs": [],
   "execution_count": 8
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 模型构建",
   "id": "9b8c14fb0804db08"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "功能: 定义了一个 Wide & Deep 模型，结合了 Wide 部分（原始特征）和 Deep 部分（多层神经网络提取的特征）。\n",
    "\n",
    "原理:\n",
    "\n",
    "Wide 部分直接使用原始特征，捕捉低阶特征交互。\n",
    "\n",
    "Deep 部分通过多层神经网络提取高阶特征交互。\n",
    "\n",
    "将 Wide 部分和 Deep 部分的特征拼接后，通过输出层得到最终结果。\n",
    "\n",
    "作用:\n",
    "\n",
    "适用于推荐系统、分类/回归任务等。\n",
    "\n",
    "能够同时捕捉低阶和高阶特征交互。\n",
    "\n",
    "为什么这样做:\n",
    "\n",
    "Wide & Deep 模型结合了传统模型的记忆能力（Wide 部分）和深度模型的泛化能力（Deep 部分）。\n",
    "\n",
    "通过参数初始化和拼接特征，增强模型的表达能力和训练稳定性。"
   ],
   "id": "6434d16ba025f39"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:45:19.081399Z",
     "start_time": "2025-02-07T01:45:19.076421Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#回归模型我们只需要1个数\n",
    "# 定义一个名为 WideDeep 的类，继承自 nn.Module\n",
    "class WideDeep(nn.Module):\n",
    "\n",
    "    # 定义类的构造函数，接受一个参数 input_dim，表示输入特征的维度\n",
    "    # 初始化模型的层结构和参数\n",
    "    def __init__(self, input_dim=8):\n",
    "        super().__init__()\n",
    "\n",
    "        # 定义 Deep 部分，使用 nn.Sequential 构建一个包含两个全连接层和 ReLU 激活函数的神经网络。\n",
    "        # Deep 部分用于提取输入特征的高阶交互信息\n",
    "        self.deep = nn.Sequential(\n",
    "            nn.Linear(input_dim, 30),\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(30, 30),\n",
    "            nn.ReLU()\n",
    "        )\n",
    "\n",
    "        # 定义输出层，输入维度为 30 + input_dim，输出维度为 1。\n",
    "        # 将 Wide 部分（原始输入特征）和 Deep 部分（提取的特征）拼接后，通过一个全连接层输出最终结果。\n",
    "        # 结合 Wide 部分和 Deep 部分的特征，增强模型的表达能力\n",
    "        self.output_layer = nn.Linear(30 + input_dim, 1)\n",
    "\n",
    "        # 调用 init_weights 方法初始化模型参数\n",
    "        self.init_weights()\n",
    "\n",
    "    # 初始化模型参数的方法，使用 Xavier 初始化方法初始化权重\n",
    "    def init_weights(self):\n",
    "\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, nn.Linear):\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                nn.init.zeros_(m.bias)\n",
    "\n",
    "    def forward(self, x):\n",
    "\n",
    "        # 将输入数据 x 传递给 Deep 部分，得到 Deep 部分的输出 deep_output\n",
    "        # Deep 部分通过多层神经网络提取特征。提取输入数据的高阶特征交互信息。\n",
    "        deep_output = self.deep(x)\n",
    "\n",
    "        # 将原始输入特征 x 和 Deep 部分的输出 deep_output 在维度 1 上拼接，使用 torch.cat 将两个张量拼接在一起\n",
    "        # 结合 Wide 部分（原始特征）和 Deep 部分（提取的特征），增强模型的表达能力\n",
    "        concat = torch.cat([x, deep_output], dim=1)\n",
    "\n",
    "        # 将拼接后的特征传递给输出层，得到最终的输出 logits\n",
    "        # 输出层将拼接后的特征映射到最终的输出维度\n",
    "        logits = self.output_layer(concat)\n",
    "        return logits"
   ],
   "id": "e4522201cfbe53f5",
   "outputs": [],
   "execution_count": 9
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:45:39.535363Z",
     "start_time": "2025-02-07T01:45:39.529227Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# train_ds[0][0]\n",
    "#验证模型是否正确\n",
    "input = train_ds[0][0].reshape(1, -1)\n",
    "print(input.shape)\n",
    "model = WideDeep()\n",
    "out = model(input)\n",
    "out.shape"
   ],
   "id": "9f5942f10ae464e4",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([1, 8])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 1])"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 10
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:46:07.467030Z",
     "start_time": "2025-02-07T01:46:07.463445Z"
    }
   },
   "cell_type": "code",
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        self.patience = patience  #容忍度\n",
    "        self.min_delta = min_delta  #最小变化量\n",
    "        self.best_metric = -1  #最佳指标\n",
    "        self.counter = 0  #计数器\n",
    "\n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            self.best_metric = metric  #更新最佳指标\n",
    "            self.counter = 0  #计数器清零\n",
    "        else:\n",
    "            self.counter += 1  #计数器加一\n",
    "\n",
    "    @property  #定义一个属性，用于判断是否满足early stop条件\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience"
   ],
   "id": "cbd068b77b773e6",
   "outputs": [],
   "execution_count": 11
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T01:46:12.898434Z",
     "start_time": "2025-02-07T01:46:12.895033Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "\n",
    "@torch.no_grad()  #不计算梯度\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []  #损失列表\n",
    "    for datas, labels in dataloader:  # 遍历验证集\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)  # 验证集损失\n",
    "        loss_list.append(loss.item())  # 损失列表添加损失值\n",
    "\n",
    "    return np.mean(loss_list)  # 计算平均损失值"
   ],
   "id": "8f6816776433aa78",
   "outputs": [],
   "execution_count": 12
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 训练模型",
   "id": "1325362f9d20a816"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T02:12:59.138561Z",
     "start_time": "2025-02-07T02:12:48.898401Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 训练\n",
    "def training(\n",
    "        model,  #模型\n",
    "        train_loader,  #训练集\n",
    "        val_loader,  #验证集\n",
    "        epoch,  #训练轮数\n",
    "        loss_fct,  #损失函数\n",
    "        optimizer,  #优化器\n",
    "        early_stop_callback=None,  #早停回调函数\n",
    "        eval_step=500,  #每隔多少步进行一次验证集验证\n",
    "):\n",
    "    # 记录训练过程\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "\n",
    "    global_step = 0  #全局步数\n",
    "    model.train()  #训练模式\n",
    "\n",
    "    # 训练\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)  #得到预测值\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "\n",
    "                loss = loss.cpu().item()  #转为cpu类型，item()是取值\n",
    "                # 记录训练过程\n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "\n",
    "                # 评估\n",
    "                if global_step % eval_step == 0:  #每隔eval_step步进行一次验证集验证\n",
    "                    model.eval()  #验证模式\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)  #验证集损失\n",
    "\n",
    "                    # 记录验证集过程\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "\n",
    "                    model.train()  #训练模式\n",
    "\n",
    "                    # 早停 Early Stop\n",
    "                    if early_stop_callback is not None:  #如果有早停回调函数\n",
    "                        early_stop_callback(-val_loss)  #根据验证集的损失来实现早停\n",
    "                        if early_stop_callback.early_stop:  #如果早停条件满足\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "\n",
    "                # 全局步数加一\n",
    "                global_step += 1\n",
    "                pbar.update(1)  #更新进度条\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})  #设置进度条显示信息\n",
    "\n",
    "    return record_dict  #返回训练过程记录\n",
    "\n",
    "\n",
    "epoch = 10\n",
    "\n",
    "model = WideDeep()  #实例化模型\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失,均方误差\n",
    "loss_fct = nn.MSELoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.0)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=10, min_delta=1e-3)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model,\n",
    "    train_loader,\n",
    "    val_loader,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    ")"
   ],
   "id": "c53abaf47fb774e9",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/7260 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "20be01a31db64eb0ab91a0508c4c9696"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n"
     ]
    }
   ],
   "execution_count": 25
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T02:13:03.753685Z",
     "start_time": "2025-02-07T02:13:03.691960Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "# 定义一个名为 plot_learning_curves 的函数，接受以下参数：\n",
    "# record_dict: 包含训练和验证记录的字典。\n",
    "# sample_step: 对训练数据进行采样的步长，默认值为 500。\n",
    "# 该函数用于可视化训练和验证过程中的性能指标。\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # 将训练记录转换为 Pandas DataFrame，并设置 step 列为索引。然后对数据进行采样，步长为 sample_step\n",
    "    # pd.DataFrame(record_dict[\"train\"]): 将训练记录转换为 DataFrame。\n",
    "    # .set_index(\"step\"): 将 step 列设置为索引。\n",
    "    # .iloc[::sample_step]: 对数据进行采样，每隔 sample_step 步取一个样本。\n",
    "    # 减少数据点的数量，使曲线更清晰，同时保留趋势\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "\n",
    "    # 将验证记录转换为 Pandas DataFrame，并设置 step 列为索引\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # 遍历训练 DataFrame 的每一列（即每个性能指标）。\n",
    "    # 使用 enumerate 获取列名和索引\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        # train_df.index: x 轴数据（训练步数）。\n",
    "        # train_df[item]: y 轴数据（性能指标值）。\n",
    "        # label=f\"train_{item}\": 设置曲线的标签。\n",
    "        plt.plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        plt.plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "\n",
    "        plt.grid()  # 在当前子图上添加网格线\n",
    "        plt.legend()  # 在当前子图上添加图例\n",
    "\n",
    "        # 生成 x 轴的刻度数据，从 0 到最大步数，步长为 5000\n",
    "        x_data = range(0, train_df.index[-1], 5000)\n",
    "\n",
    "        plt.xticks(x_data)  # 设置 x 轴的刻度位置\n",
    "        plt.xticks(range(0, train_df.index[-1], 10 * sample_step), range(0, train_df.index[-1], 10 * sample_step))\n",
    "        plt.xlabel(\"step\")  # 设置 x 轴的标签为“step”\n",
    "\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "plot_learning_curves(record)"
   ],
   "id": "60d4cff7c80a5617",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGwCAYAAAD16iy9AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAATCtJREFUeJzt3Qd4W9X9PvBXluW994jt7D0gAUIYISGLsKEtFNIyOih/ktISoC0dQKAQSik/KKVAoRAoUFYJpRBCQiZkkEXIHnYcx0nsON7bliX9n++RpXjItmRfWev9PM99tK6l60S+enXO95yjs1gsFhARERFpIEiLJyEiIiISDBZERESkGQYLIiIi0gyDBREREWmGwYKIiIg0w2BBREREmmGwICIiIs0Eo5+ZzWacPHkS0dHR0Ol0/f3yRERE1Asy7VVNTQ0yMjIQFBTkPcFCQkVWVlZ/vywRERFpoLCwEAMGDPCeYCEtFbYDi4mJ0ex5jUYjVqxYgdmzZ8NgMGj2vEREvoLnQXKn6upq1TBg+xz3mmBh6/6QUKF1sIiIiFDPyT8oIgpEPA9Sf+ipjIHFm0RERKQZBgsiIiLSDIMFERERaabfayyIiMj/yFQCzc3Nnj4M6gOpy9Hr9egrBgsiIuoTCRT5+fkqXJBvi4uLQ1paWp/mmWKwICKiPk2aVFRUpL7pylDE7iZOIu/+f6yvr0dJSYm6nZ6e3uvnYrAgIqJea2lpUR9IMhujDHUl3xUeHq4uJVykpKT0uluE0ZKIiHrNZDKpy5CQEE8fCmnAFg5lTpTeYrAgIqI+49pP/kGnwf8jgwURERFphsGCiIiINMNgQURE1AcDBw7EM888o8lzrV27VnVHVFZWwlf5z6gQYz1i6gsAsxQScfEdIiLq2rRp03DWWWdpEgi2bt2KyMhITY7LH/hHsDCbEfz0cExvaYSx+nIgeainj4iIiHx8XgcZ8RIc3PPHZHJycr8ck6/wj64QmZAlfpC6qis97OmjISIK7ImWmls8sslrO+O2227DunXr8Oyzz6puB9mWLFmiLj/77DNMmjQJoaGh+Oqrr5CXl4drrrkGqampiIqKwrnnnosvvvii264QnU6HV155Bdddd50avjls2DB8/PHHvf43/c9//oMxY8aoY5LX+stf/tLu8b///e/qNcLCwtRxfve737U/9sEHH2DcuHFqjorExETMnDkTdXV1cKdgv3kzJw6D7vR+6MokWMz19OEQEQWkBqMJox/83COvve+ROYgI6fljTQLFoUOHMHbsWDzyyCPqvr1796rL3/zmN3jqqacwePBgxMfHo7CwEJdffjkee+wx9cH+xhtv4KqrrsLBgweRnZ3d5WssWrQITz75JP785z/jueeew7x581BQUICEhASXfqft27fjhhtuwMMPP4wbb7wRGzduxF133aVCggSkbdu24e6778a//vUvXHDBBSgvL8eXX36pflZmRL3pppvUcUjIqampUY85G8B6y6+ChaKCBRERkWOxsbFqQi9pTZB1McSBAwfUpQSNWbNm2feVIDBhwgT77UcffRRLly5VLRALFizo8jVuu+029aEuHn/8cfz1r3/Fli1bcNlll7l0rE8//TRmzJiBP/zhD+r28OHDsW/fPhVY5DWOHTum6juuvPJKREdHIycnB2effbY9WMjMqNdff726X0jrhbv5T7BIstZVWFssiIjIE8INetVy4KnX7qtzzjmn3e3a2lrVWvDpp5/aP6gbGhrUB3p3xo8fb78uH/wxMTH2dThcsX//ftUV09aFF16oul6kBkRCkIQGaWGR0CKbrQtGApGEEgkTc+bMwezZs1U3ibTEuJN/1FiIBFuwyPP0kRARBSypL5DuCE9sWswa2XF0x3333adaKKTVQboRdu7cqT6oe1oi3mBoPzpRjs0dq79KK8WOHTvw73//Wy0c9uCDD6pAIcNVZa2PlStXqrqR0aNHqy6ZESNGqJVo3clvgoUlsTVY1JUADRWePhwiIvJi0hViW+ekOxs2bFBdDtIKIIFCuk6OHj2K/jJq1Ch1DB2PSbpEbIuEycgVKcqUWopdu3ap41u9erU90EgLh9R8fPPNN+r3lqDkTn7TFYLQaDQY4hFurABKc4Gscz19RERE5KVkdMXXX3+tPoRltEdXrQky2uLDDz9UBZvyIS21Du5oeejKvffeq0aiSG2HFG9u2rQJf/vb39RIEPHJJ5/gyJEjmDp1quriWLZsmTo+aZmQ32/VqlWqC0RWK5Xbp0+fVmHFnfymxULUhrauH196yNOHQkREXky6OOQbv3QRyDwUXdVMSPGkfGDLiAsJF1KrMHHixH47zokTJ+K9997DO++8o0axSFeHFJhKK4qIi4tTwefSSy9VgeHFF19U3SIyPFXqOtav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EDfccAPi4uKQkJCAa665BkePHu3U0rFo0SIkJyerZbzvvPNONDc3e/C3JCKtBWv+jETk84qKinDTTTfhySefxHXXXYeamhp8+eWXuOWWW3Ds2DFUV1fjtddeU/tKiDAajZgzZw6mTJmi9gsODsYf//hHXHbZZdi1axdCQkLUvqtWrUJYWJgKIxI6br/9dhVaHnvsMQ//xkSkFQYLInIYLFpaWnD99dcjJydH3SetF0JaMJqampCWlmbf/80334TZbMYrr7yiWjGEBA9pvZAQMXv2bHWfBIxXX30VERERGDNmDB555BHVCvLoo48iKIgNqET+gH/JRNTJhAkTMGPGDBUmvve97+Hll19GRUVFl/t/++23yM3NRXR0NKKiotQmLRmNjY3Iy8tr97wSKmykhaO2tlZ1oxCRf2CLBRF1otfrsXLlSmzcuBErVqzAc889h9/97nf4+uuvHe4v4WDSpEl46623Oj0m9RREFDgYLIjIIenSuPDCC9X24IMPqi6RpUuXqu4Mk8nUbt+JEyfi3XffRUpKiirK7K5lo6GhQXWniM2bN6vWjaysLLf/PkTUP9gVQkSdSMvE448/jm3btqlizQ8//BCnT5/GqFGjMHDgQFWQefDgQZSWlqrCzXnz5iEpKUmNBJHizfz8fFVbcffdd+P48eP255URID/+8Y+xb98+LFu2DA899BAWLFjA+goiP8IWCyLqRFod1q9fj2eeeUaNAJHWir/85S+YO3cuzjnnHBUa5FK6QNasWYNp06ap/X/961+rgk8ZRZKZmanqNNq2YMjtYcOGYerUqaoAVEaePPzwwx79XYlIWzqLxWLR+DmJiDqReSwqKyvx0UcfefpQiMiN2P5IREREmmGwICIiIs2wK4SIiIg0wxYLIiIi0gyDBREREWmGwYKIiIg0w2BBREREmmGwICIiIs0wWBAREZFmGCyIiIhIMwwWREREBK38fzWKJR9RAwBvAAAAAElFTkSuQmCC"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 26
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-07T02:13:06.086085Z",
     "start_time": "2025-02-07T02:13:06.044278Z"
    }
   },
   "cell_type": "code",
   "source": [
    "model.eval()\n",
    "loss = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\")"
   ],
   "id": "abd8a9db98640600",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.5012\n"
     ]
    }
   ],
   "execution_count": 27
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "",
   "id": "ddd81cf800ec3f6c"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
